# This integration spec isn't *quite* about exception handling. It's more of a
# regression test, since Goliath::Rack::Params used to swallow any exception
# generated downstream from it in the middleware chain (even though
# Goliath::API#call handles exceptions already).

require 'spec_helper'

class ExceptionHandlingMiddleware
  include Goliath::Rack::AsyncMiddleware
  include Goliath::Constants

  def call(env)
    # This kind of relies on downstream middleware raising their own exceptions
    # if something goes wrong. Alternatively, they could rescue the exception
    # and set env[RACK_EXCEPTION]. See #post_process.
    super
  rescue Exception => e
    handle_exception(e)
  end

  def post_process(env, status, headers, body)
    # If an exception was raised by Goliath::API#call, it will store the
    # exception in env[RACK_EXCEPTION] and automatically generate an error
    # response. We circumvent the usual error response by returning our own.
    return handle_exception(env[RACK_EXCEPTION]) if env[RACK_EXCEPTION]
    [status, headers, body]
  end

  private

  def handle_exception(e)
    # You could imagine this middleware having some sort of side effect, like
    # sending your team an email alert detailing the exception. For the
    # purposes of the spec, we'll just return some text.
    [200, {}, "Exception raised: #{e.message}"]
  end
end

class ExceptionRaisingMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    raise env.params['raise'] if env.params['raise']
    @app.call(env)
  end
end

class ExceptionHandlingAPI < Goliath::API
  # The exception handling middleware should come first in the chain so that
  # any exception in the rest of the chain gets rescued.
  use ExceptionHandlingMiddleware

  # Require param parsing before the exception raising middleware, because the
  # latter depends on params being parsed. Herein lies the regression:
  # Goliath::Rack::Params used to swallow any exceptions raised downstream from
  # it (here, ExceptionRaisingMiddleware), so any upstream middleware (here,
  # ExceptionHandlingMiddleware) would be none the wiser. Really, the only
  # thing Goliath::Rack::Params should be worried about is whether it can parse
  # the params.
  use Goliath::Rack::Params

  # Allow us to raise an exception on demand with the param raise=<message>.
  # You could imagine something less dumb for a real-life application that
  # incidentally raises an exception, if you'd like.
  use ExceptionRaisingMiddleware

  def response(env)
    # Goliath::API#call ensures that any exceptions raised here get rescued and
    # stored in env[RACK_EXCEPTION].
    fail env.params['fail'] if env.params['fail']
    [200, {}, 'No exceptions raised']
  end
end

describe ExceptionHandlingAPI do
  let(:err) { Proc.new { fail 'API request failed' } }

  context 'when no exceptions get raised' do
    let(:query) { '' }

    it 'returns a normal response' do
      with_api(ExceptionHandlingAPI) do
        get_request({ query: query }, err) do |c|
          expect(c.response_header.status).to eq(200)
          expect(c.response).to eq('No exceptions raised')
        end
      end
    end
  end

  context 'when a middleware raises an exception' do
    let(:query) { 'raise=zoinks' }

    it 'handles the exception using ExceptionHandlingMiddleware' do
      with_api(ExceptionHandlingAPI) do
        get_request({ query: query }, err) do |c|
          expect(c.response_header.status).to eq(200)
          expect(c.response).to eq('Exception raised: zoinks')
        end
      end
    end
  end

  context 'when the API raises an exception' do
    let(:query) { 'fail=jinkies' }

    it 'handles the exception using ExceptionHandlingMiddleware' do
      with_api(ExceptionHandlingAPI) do
        get_request({ query: query }, err) do |c|
          expect(c.response_header.status).to eq(200)
          expect(c.response).to eq('Exception raised: jinkies')
        end
      end
    end
  end

  context 'when param parsing raises an exception' do
    let(:query) { 'ambiguous[]=&ambiguous[4]=' }

    it 'returns a validation error generated by Goliath::Rack::Params' do
      with_api(ExceptionHandlingAPI) do
        get_request({ query: query }, err) do |c|
          expect(c.response_header.status).to eq(400)
          expect(c.response).to eq("[:error, \"Invalid parameters: Rack::Utils::ParameterTypeError\"]")
        end
      end
    end
  end
end

# This API has the same setup as ExceptionHandlingAPI, except for the crucial
# difference that it does *not* use ExceptionHandlingMiddleware. This is to
# make sure that exceptions are still rescued somewhere in an API call, even
# though they aren't getting swallowed by Goliath::Rack::Params anymore.

class PassiveExceptionHandlingAPI < Goliath::API
  use Goliath::Rack::Params
  use ExceptionRaisingMiddleware

  def response(env)
    fail env.params['fail'] if env.params['fail']
    [200, {}, 'No exceptions raised']
  end
end

describe PassiveExceptionHandlingAPI do
  let(:err) { Proc.new { fail 'API request failed' } }

  context 'when no exceptions get raised' do
    let(:query) { '' }

    it 'returns a normal response' do
      with_api(PassiveExceptionHandlingAPI) do
        get_request({ query: query }, err) do |c|
          expect(c.response_header.status).to eq(200)
          expect(c.response).to eq('No exceptions raised')
        end
      end
    end
  end

  context 'when a middleware raises an exception' do
    let(:query) { 'raise=ruh-roh' }

    it 'returns the server error generated by Goliath::Request#process' do
      with_api(PassiveExceptionHandlingAPI) do
        get_request({ query: query }, err) do |c|
          expect(c.response_header.status).to eq(500)
          expect(c.response).to eq('ruh-roh')
        end
      end
    end
  end

  context 'when the API raises an exception' do
    let(:query) { 'fail=puppy-power' }

    it 'returns the validation error generated by Goliath::API#call' do
      with_api(PassiveExceptionHandlingAPI) do
        get_request({ query: query }, err) do |c|
          expect(c.response_header.status).to eq(500)
          expect(c.response).to eq('[:error, "puppy-power"]')
        end
      end
    end
  end

  context 'when param parsing raises an exception' do
    let(:query) { 'ambiguous[]=&ambiguous[4]=' }

    it 'returns a validation error generated by Goliath::Rack::Params' do
      with_api(PassiveExceptionHandlingAPI) do
        get_request({ query: query }, err) do |c|
          expect(c.response_header.status).to eq(400)
          expect(c.response).to eq('[:error, "Invalid parameters: Rack::Utils::ParameterTypeError"]')
        end
      end
    end
  end
end
